<!DOCTYPE html>
<html>
<head>
<title>ChangeLog Analysis</title>
<style type="text/css">

body {
    font-family: 'Helvetica' 'Segoe UI Light' sans-serif;
    font-weight: 200;
    padding: 20px;
    min-width: 1200px;
}

* {
    padding: 0px;
    margin: 0px;
    border: 0px;
}

h1, h2, h3 {
    font-weight: 200;
}

h1 {
    margin: 0 0 1em 0;
}

h2 {
    font-size: 1.2em;
    text-align: center;
    margin-bottom: 1em;
}

h3 {
    font-size: 1em;
}

.view {
    margin: 0px;
    width: 600px;
    float: left;
}

.graph-container p {
    width: 200px;
    text-align: right;
    margin: 20px 0 20px 0;
    padding: 5px;
    border-right: solid 1px black;
}

.graph-container table {
    width: 100%;
}

.graph-container table, .graph-container td {
    border-collapse: collapse;
    border: none;
}

.graph-container td {
    padding: 5px;
    vertical-align: center;
}

.graph-container td:first-child {
    width: 200px;
    text-align: right;
    border-right: solid 1px black;
}

.graph-container .selected {
    background: #eee;
}

#reviewers .selected td:first-child {
    border-radius: 10px 0px 0px 10px;
}

#areas .selected td:last-child {
    border-radius: 0px 10px 10px 0px;
}

.graph-container .bar {
    display: inline-block;
    min-height: 1em;
    background: #9f6;
    margin-right: 0.4ex;
}

.graph-container .reviewed-patches {
    background: #3cf;
    margin-right: 1px;
}

.graph-container .unreviewed-patches {
    background: #f99;
}

.constrained {
    background: #eee;
    border-radius: 10px;
}

.constrained .vertical-bar {
    border-right: solid 1px #eee;
}

#header {
    border-spacing: 5px;
}

#header section {
    display: table-cell;
    width: 200px;
    vertical-align: top;
    border: solid 2px #ccc;
    border-collapse: collapse;
    padding: 5px;
    font-size: 0.8em;
}

#header dt {
    float: left;
}

#header dt:after {
    content: ': ';
}

#header .legend {
    width: 600px;
}

.legend .bar {
    width: 15ex;
    padding: 2px;
}

.legend .reviews {
    width: 25ex;
}

.legend td:first-child {
    width: 18ex;
}

</style>
</head>
<body>
<h1>ChangeLog Analysis</h1>

<section id="header">
<section id="summary">
<h2>Summary</h2>
</section>

<section class="legend">
<h2>Legend</h2>
<div class="graph-container">
<table>
<tbody>
<tr><td>Contributor's name</td>
<td><span class="bar reviews">Reviews</span> <span class="value-container">(# of reviews)</span><br>
<span class="bar reviewed-patches">Reviewed</span><span class="bar unreviewed-patches">Unreviewed</span>
<span class="value-container">(# of reviewed):(# of unreviewed)</span></td></tr>
</tbody>
</table>
</div>
</section>
</section>

<section id="contributors" class="view">
<h2 id="contributors-title">Contributors</h2>
<div class="graph-container"></div>
</section>

<section id="areas" class="view">
<h2 id="areas-title">Areas of contributions</h2>
<div class="graph-container"></div>
</section>

<script>

// Naive implementation of element extensions discussed on public-webapps

if (!Element.prototype.append) {
    Element.prototype.append = function () {
        for (var i = 0; i < arguments.length; i++) {
            // FIXME: Take care of other node types
            if (arguments[i] instanceof Element || arguments[i] instanceof CharacterData)
                this.appendChild(arguments[i]);
            else
                this.appendChild(document.createTextNode(arguments[i]));
        }
        return this;
    }
}

if (!Node.prototype.remove) {
    Node.prototype.remove = function () {
        this.parentNode.removeChild(this);
        return this;
    }
}

if (!Element.create) {
    Element.create = function () {
        if (arguments.length < 1)
            return null;
        var element = document.createElement(arguments[0]);
        if (arguments.length == 1)
            return element;

        // FIXME: the second argument can be content or IDL attributes
        var attributes = arguments[1];
        for (attribute in attributes)
            element.setAttribute(attribute, attributes[attribute]);

        if (arguments.length >= 3)
            element.append.apply(element, arguments[2]);

        return element;
    }
}

if (!Node.prototype.removeAllChildren) {
    Node.prototype.removeAllChildren = function () {
        while (this.firstChild)
            this.firstChild.remove();
        return this;
    }
}

Element.prototype.removeClassNameFromAllElements = function (className) {
    var elements = this.getElementsByClassName(className);
    for (var i = 0; i < elements.length; i++)
        elements[i].classList.remove(className);
}

function getJSON(url, callback) {
    var xhr = new XMLHttpRequest();
    xhr.open('GET', url, true);
    xhr.onreadystatechange = function () {
        if (this.readyState == 4)
            callback(JSON.parse(xhr.responseText));
    }
    xhr.send();
}

function GraphView(container) {
    this._container = container;
    this._defaultData = null;
}

GraphView.prototype.setData = function(data, constrained) {
    if (constrained)
        this._container.classList.add('constrained');
    else
        this._container.classList.remove('constrained');
    this._clearGraph();
    this._constructGraph(data);
}

GraphView.prototype.setDefaultData = function(data) {
    this._defaultData = data;
    this.setData(data);
}

GraphView.prototype.reset = function () {
    this.setMarginTop();
    this.setData(this._defaultData);
}

GraphView.prototype.isConstrained = function () { return this._container.classList.contains('constrained'); }

GraphView.prototype.targetRow = function (node) {
    var target = null;

    while (node && node != this._container) {
        if (node.localName == 'tr')
            target = node;
        node = node.parentNode;
    }

    return node && target;
}

GraphView.prototype.selectRow = function (row) {
    this._container.removeClassNameFromAllElements('selected');
    row.classList.add('selected');
}

GraphView.prototype.setMarginTop = function (y) { this._container.style.marginTop = y ? y + 'px' : null; }
GraphView.prototype._graphContainer = function () { return this._container.getElementsByClassName('graph-container')[0]; }
GraphView.prototype._clearGraph = function () { return this._graphContainer().removeAllChildren(); }

GraphView.prototype._numberOfPatches = function (dataItem) {
    return dataItem.numberOfReviewedPatches + (dataItem.numberOfUnreviewedPatches !== undefined ? dataItem.numberOfUnreviewedPatches : 0);
}

GraphView.prototype._maximumValue = function (labels, data) {
    var numberOfPatches = this._numberOfPatches;
    return Math.max.apply(null, labels.map(function (label) {
        return Math.max(numberOfPatches(data[label]), data[label].numberOfReviews !== undefined ? data[label].numberOfReviews : 0);
    }));
}

GraphView.prototype._sortLabelsByNumberOfReviwsAndReviewedPatches = function(data) {
    var labels = Object.keys(data);
    if (!labels.length)
        return null;
    var numberOfPatches = this._numberOfPatches;
    var computeValue = function (dataItem) {
        return numberOfPatches(dataItem) + (dataItem.numberOfReviews !== undefined ? dataItem.numberOfReviews : 0);
    }
    labels.sort(function (a, b) { return computeValue(data[b]) - computeValue(data[a]); });
    return labels;
}

GraphView.prototype._constructGraph = function (data) {
    var element = this._graphContainer();
    var labels = this._sortLabelsByNumberOfReviwsAndReviewedPatches(data);
    if (!labels) {
        element.append(Element.create('p', {}, ['None']));
        return;
    }

    var maxValue = this._maximumValue(labels, data);
    var computeStyleForBar = function (value) { return 'width:' + (value * 85.0 / maxValue) + '%' }

    var table = Element.create('table', {}, [Element.create('tbody')]);
    for (var i = 0; i < labels.length; i++) {
        var label = labels[i];
        var item = data[label];
        var row = Element.create('tr', {}, [Element.create('td', {}, [label]), Element.create('td', {})]);
        var valueCell = row.lastChild;

        if (item.numberOfReviews != undefined) {
            valueCell.append(
                Element.create('span', {'class': 'bar reviews', 'style': computeStyleForBar(item.numberOfReviews) }),
                Element.create('span', {'class': 'value-container'}, [item.numberOfReviews]),
                Element.create('br')
            );
        }

        valueCell.append(Element.create('span', {'class': 'bar reviewed-patches', 'style': computeStyleForBar(item.numberOfReviewedPatches) }));
        if (item.numberOfUnreviewedPatches !== undefined)
            valueCell.append(Element.create('span', {'class': 'bar unreviewed-patches', 'style': computeStyleForBar(item.numberOfUnreviewedPatches) }));

        valueCell.append(Element.create('span', {'class': 'value-container'},
            [item.numberOfReviewedPatches + (item.numberOfUnreviewedPatches !== undefined ? ':' + item.numberOfUnreviewedPatches : '')]));

        table.firstChild.append(row);
        row.label = label;
        row.data = item;
    }
    element.append(table);
}

var contributorsView = new GraphView(document.querySelector('#contributors'));
var areasView = new GraphView(document.querySelector('#areas'));

getJSON('summary.json',
    function (summary) {
        var summaryContainer = document.querySelector('#summary');
        summaryContainer.append(Element.create('dl', {}, [
            Element.create('dt', {}, ['Total entries (reviewed)']),
            Element.create('dd', {}, [(summary['reviewed'] + summary['unreviewed']) + ' (' + summary['reviewed'] + ')']),
            Element.create('dt', {}, ['Total contributors']),
            Element.create('dd', {}, [summary['contributors']]),
            Element.create('dt', {}, ['Contributors who reviewed']),
            Element.create('dd', {}, [summary['contributors_with_reviews']]),
        ]));
    });

getJSON('contributors.json',
    function (contributors) {
        for (var contributor in contributors) {
            contributor = contributors[contributor];
            contributor.numberOfReviews = contributor.reviews ? contributor.reviews.total : 0;
            contributor.numberOfReviewedPatches = contributor.patches ? contributor.patches.reviewed : 0;
            contributor.numberOfUnreviewedPatches = contributor.patches ? contributor.patches.unreviewed : 0;
        }
        contributorsView.setDefaultData(contributors);
    });

getJSON('areas.json',
    function (areas) {
        for (var area in areas) {
            areas[area].numberOfReviewedPatches = areas[area].reviewed;
            areas[area].numberOfUnreviewedPatches = areas[area].unreviewed;
        }
        areasView.setDefaultData(areas);
    });

function contributorAreas(contributorData) {
    var areas = new Object;
    for (var area in contributorData.reviews.areas) {
        if (!areas[area])
            areas[area] = {'numberOfReviewedPatches': 0};
        areas[area].numberOfReviews = contributorData.reviews.areas[area];
    }
    for (var area in contributorData.patches.areas) {
        if (!areas[area])
            areas[area] = {'numberOfReviews': 0};
        areas[area].numberOfReviewedPatches = contributorData.patches.areas[area];
    }
    return areas;
}

function areaContributors(areaData) {
    var contributors = areaData['contributors'];
    for (var contributor in contributors) {
        contributor = contributors[contributor];
        contributor.numberOfReviews = contributor.reviews;
        contributor.numberOfReviewedPatches = contributor.reviewed;
        contributor.numberOfUnreviewedPatches = contributor.unreviewed;
    }
    return contributors;
}

var mouseTimer = 0;
window.onmouseover = function (event) {
    clearTimeout(mouseTimer);

    var row = contributorsView.targetRow(event.target);
    if (row) {
        if (!contributorsView.isConstrained()) {
            contributorsView.selectRow(row);
            areasView.setMarginTop(row.firstChild.offsetTop);
            areasView.setData(contributorAreas(row.data), 'constrained');
        }
        return;
    }

    row = areasView.targetRow(event.target);
    if (row) {
        if (!areasView.isConstrained()) {
            areasView.selectRow(row);
            contributorsView.setMarginTop(row.firstChild.offsetTop);
            contributorsView.setData(areaContributors(row.data), 'constrained');
        }
        return;
    }

    mouseTimer = setTimeout(function () {
        contributorsView.reset();
        areasView.reset();
    }, 500);
}

</script>
</body>
</html>
